﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Xml;
using System.Diagnostics;

namespace FileModelNS
{
	public class FileFormat
	{
		public string FileName { get { return _FileName; } }
		public Tree<FormatNode> Tree { get { return _Tree; } } 

		private string _FileName;
		private Tree<FormatNode> _Tree;
		private FileContents _FileContents;

		public FileFormat(string path, FileContents fileContents )
		{
			if ( null == fileContents)
				throw new Exception("Invalid file contents.");

			// Create a DOM Document and load the XML data into it.
			XmlDocument dom = new XmlDocument();
			dom.Load(path);
			XmlElement rootElement = dom.DocumentElement;

			// Validate the signature
			ValidateSignature(rootElement, fileContents);

			// Initialize a tree
			var rootNode = new TreeNode<FormatNode>(
				new FormatNode(rootElement.Attributes["name"].Value, rootElement.Name, null, 0, 0));
			var tree = new Tree<FormatNode>();
			tree.RootNodes.Add(rootNode);

			// Set instance variables beforing calling FillNodeAndChilds().
			// Recursive function FillNodeAndChilds() refers to these variables.
			_Tree = tree;
			_FileContents = fileContents;

			// Init member
			// Populate the tree with the DOM nodes.
			FillNodeAndChilds(rootNode, rootElement, 0);
			
			// Succeeded
			_FileName = path;
		}

		private void ValidateSignature(XmlNode rootNode, FileContents fileContents)
		{
			if (rootNode.Attributes["signature"] != null)
			{
				if (rootNode.Attributes["signature_offset"] == null)
				{
					throw new Exception(
						"signature attribute is defined, but signature_offset is not defined.\nPlease make sure that the signature_offset attribute is defined");
				}

				byte[] signatureInFormat = BaseUtility.ConvertHexValueToByteArray(rootNode.Attributes["signature"].Value);
				int offset = int.Parse(rootNode.Attributes["signature_offset"].Value);

				byte[] signatureInFile = fileContents.CopyBytes(offset, signatureInFormat.Length);
				if (!BaseUtility.CompareArray(signatureInFormat, signatureInFile))
					throw new Exception("Invalid file format. Signature is not identical.");
			}
		}

		private void FillNodeAndChilds(TreeNode<FormatNode> inTreeNode, XmlNode inXmlNode, int offset)
		{
			int start_offset = offset;
			int inOffset = offset;

			if (inXmlNode.Attributes["offset"] != null)
			{
				string strValue = inXmlNode.Attributes["offset"].Value;
				inOffset = CalculateValue(strValue);
			}

			// Loop through the XML nodes until the leaf is reached.
			// Add the nodes to the TreeView during the looping process.
			if (inXmlNode.HasChildNodes)
			{
				int size = 0;

				// offset 값을 직접 가져오도록 한다.
				int childOffset = inOffset;
				int node_count = 0;
				for (int i = 0; i < inXmlNode.ChildNodes.Count; i++)
				{
					if (inXmlNode.ChildNodes[node_count].NodeType != XmlNodeType.Element)
						continue;

					XmlNode curXmlNode = inXmlNode.ChildNodes[node_count];

					// Repeat check
					//
					if (curXmlNode.Name == "FM_COMMAND_REPEAT")
					{
						int count = CalculateValue(curXmlNode.Attributes["count"].Value);

						int node_offset = inTreeNode.ChildNodes.Count;
						childOffset = inTreeNode.ChildNodes[node_offset - 1].Value.Offset 
							+ inTreeNode.ChildNodes[node_offset - 1].Value.Size;

						for (int repeat = 0; repeat < count; repeat++)
						{
							// Create a new tree node and insert it into the tree
							// You should fill 'Text' and 'Path' properties before calling FillNodeAndChilds().
							// You could fill 'Offset' and 'Comment' now or later.
							// but, you cannot fill 'Size' before calling FillNodeAndChilds().
							var curTreeNode = new TreeNode<FormatNode>(
								new FormatNode(
									curXmlNode.ChildNodes[0].Name + "(" + repeat + ")",
									inTreeNode.Value.Path + "\\" + curXmlNode.ChildNodes[0].Name + "(" + repeat + ")",
									null, 0, 0
									)
								);
							inTreeNode.ChildNodes.Add( curTreeNode );

							// Recursive Call.
							FillNodeAndChilds(curTreeNode, curXmlNode.ChildNodes[0], childOffset);

							childOffset += curTreeNode.Value.Size;
							size += curTreeNode.Value.Size;
						}
					}
					else
					{
						// Create a new tree node and insert it into the tree
						// You should fill 'Path' property before calling FillNodeAndChilds().
						// You could fill 'Text', 'Offset' and 'Comment' now or later.
						// but, you cannot fill 'Size' before calling FillNodeAndChilds().
						var curTreeNode = new TreeNode<FormatNode>(
							new FormatNode(
								null,
								inTreeNode.Value.Path + "\\" + curXmlNode.Name,
								null, 0, 0
								)
							);
						inTreeNode.ChildNodes.Add( curTreeNode );

						// Recursive Call.
						FillNodeAndChilds(curTreeNode, curXmlNode, childOffset);

						// 상위 노드의 경우, 하위 노드의 합으로 정의할 수 있다.
						// 이 때 상위 노드에 size가 정의되어 있는 경우에는
						// 상위 노드의 size 값을 그대로 사용한다.
						//
						childOffset += curTreeNode.Value.Size;
						size += curTreeNode.Value.Size;
					}

					node_count++;
				}

				// Fill myself completely.
				// You should fill 'Size' property here. (=After calling FillNodeAndChilds() )
                // And you should fill 'Offset' and 'Comment' here 
				// because you didn't fill it before calling FillNodeAndChilds().
				// 'Text' property may or may not be set.
				if (inXmlNode.Attributes["size"] == null)
				{
					// 부모 노드의 크기 값은 자식 노드의 Size의 합이 아니라,
					// 이전 offset과 최종 offset의 차이로 계산한다.
					// 왜냐하면 자식 노드가 중간에 빈 공간을 가질 수 있기 때문이다.
					inTreeNode.Value.Size = childOffset - start_offset;
				}
				else
				{
					inTreeNode.Value.Size = CalculateValue(inXmlNode.Attributes["size"].Value);
				}
				if (inXmlNode.Attributes["comment"] != null)
					inTreeNode.Value.Comment = inXmlNode.Attributes["comment"].Value;
				if (inXmlNode.Attributes["name"] != null)
					inTreeNode.Value.Text = inXmlNode.Attributes["name"].Value;
				else
					inTreeNode.Value.Text = inXmlNode.Name;
				inTreeNode.Value.Offset = inOffset;
			}
			else
			{
				// Fill myself completely.
				// And you should fill 'Size', 'Offset' and 'Comment' here 
				// because you didn't fill it before calling FillNodeAndChilds().
				// 'Text' property may or may not be set.
				if (inXmlNode.Attributes["size"] != null)
					inTreeNode.Value.Size = CalculateValue(inXmlNode.Attributes["size"].Value);
				if (inXmlNode.Attributes["comment"] != null)
					inTreeNode.Value.Comment = inXmlNode.Attributes["comment"].Value;
				if (inXmlNode.Attributes["name"] != null)
					inTreeNode.Value.Text = inXmlNode.Attributes["name"].Value;
				else
					inTreeNode.Value.Text = inXmlNode.Name;
				inTreeNode.Value.Offset = inOffset;
			}
		}

		public int CalculateValue(string path)
		{
			int ret = 0;

			string expr = path;
			char[] charSeparators = new char[] {' '};
			string[] indirectVariables =
				path.Replace('(', ' ').Replace(')', ' ').Replace('+', ' ').Replace('-', ' ').Replace('*', ' ').Replace('/', ' ').
					Replace('%', ' ').Split(charSeparators, StringSplitOptions.RemoveEmptyEntries);

			foreach (string var in indirectVariables)
			{
				int value = -1;
				var.Trim(' ');

				switch (var[0])
				{
					case '!': // Size
						value = FindPathAndReturnSize(var.TrimStart('!'));
						break;
					case '@': // Offset
						value = FindPathAndReturnOffset(var.TrimStart('@'));
						break;
					case '#': // Hex Value
						value = FindPathAndReturnValue(var.TrimStart('#'));
						break;
					default:
						value = Convert.ToInt32(var);
						break;
				}

				expr = expr.Replace(var, value.ToString());
			}

			// LearnFromCode: Calculate relative variables
			// Source Code from http://community.bartdesmet.net/blogs/bart/
			//
			// 음수로 시작하면 오류 발생!
			//
			ret = DynCalc.Calculator.GetValue(expr);

			return ret;
		}

		public int FindPathAndReturnValue(string path )
		{
			TreeNode<FormatNode> node = FindPath(path);
			if (node == null)
				throw new Exception("Invalid expression\n\n" + path + "\n\n" + "Cannot find the tree node.");

			int offset = node.Value.Offset;
			int size = node.Value.Size;
			byte[] value = _FileContents.CopyBytes(offset, size);
			if (value == null)
			{
				throw new Exception("Out of range!\nThe value of the following expression couldn't be handled properly because the size is larger than the size of the binary file.\nPlease make sure that the expression is correct\n\n" + path);
			}
			offset = 0;

			for (int i = size - 1; i >= 0; i--)
			{
				offset = offset << 8;
				offset += value[i];
			}

			return offset;
		}

		public int FindPathAndReturnSize(string path)
		{
			TreeNode<FormatNode> node = FindPath(path);
			if (node == null)
				throw new Exception("Invalid expression\n\n" + path + "\n\n" + "Cannot find the tree node.");

			return node.Value.Size;
		}

		public int FindPathAndReturnOffset(string path)
		{
			TreeNode<FormatNode> node = FindPath(path);
			if (node == null)
				throw new Exception("Invalid expression\n\n" + path + "\n\n" + "Cannot find the tree node.");

			return node.Value.Offset;
		}

		/// <summary>
		/// Find a node with path in a sub tree
		/// </summary>
		/// <param name="inTreeNode">Root node of sub tree</param>
		/// <param name="path">Path to find.</param>
		/// <returns></returns>
		public TreeNode<FormatNode> FindPath(TreeNode<FormatNode> inTreeNode, string path)
		{
			if (null == inTreeNode) throw new Exception( "Invalid Argument.");
			if (null == inTreeNode.Value) throw new Exception("Invalid Argument.");

			if (path == inTreeNode.Value.Path)
				return inTreeNode;

			foreach (TreeNode<FormatNode> childNode in inTreeNode.ChildNodes)
			{
				TreeNode<FormatNode> node = FindPath(childNode, path);
				if (null != node)
					return node;
			}

			return null;
		}

		/// <summary>
		/// Find a node with Path in all the tree
		/// </summary>
		/// <param name="str">Path to find.</param>
		/// <returns></returns>
		public TreeNode<FormatNode> FindPath(string str)
		{
			return FindPath(_Tree.RootNodes[0], str);
		}

		/// <summary>
		/// Find a nodw with offset in a sub tree
		/// </summary>
		/// <param name="inTreeNode">Root node of sub tree</param>
		/// <param name="offset">Offset to find.</param>
		/// <returns></returns>
		public TreeNode<FormatNode> FindOffset(TreeNode<FormatNode> inTreeNode, int offset )
		{
			if (null == inTreeNode) throw new Exception("Invalid Argument.");
			if (null == inTreeNode.Value) throw new Exception("Invalid Argument.");

			// Out of range
			if (offset < inTreeNode.Value.Offset ||
				offset >= inTreeNode.Value.Offset + inTreeNode.Value.Size)
				return null;

			foreach (TreeNode<FormatNode> childNode in inTreeNode.ChildNodes)
			{
				TreeNode<FormatNode> node = FindOffset(childNode, offset);
				if (null != node)
					return node;
			}

			return inTreeNode;
		}

		/// <summary>
		/// Find a Node with offset in all the tree
		/// </summary>
		/// <param name="offset"></param>
		/// <returns></returns>
		public TreeNode<FormatNode> FindOffset(int offset)
		{
			return FindOffset(_Tree.RootNodes[0], offset);
		}
	}
}
